﻿#region [===== Using =====]
using System;
using System.Collections;
using System.ComponentModel;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using System.Collections.Generic;
#endregion

namespace TPG.SharePoint.Workflow
{
    /// <summary>
    /// Represents the data of a task as a set of name / value pairs.
    /// </summary>
    /// <remarks>
    /// <para>Task data is stored inside SharePoint as a list item. The data 
    /// itself can be stored either in the fields of the list item or in a 
    /// special container with name / value pairs known as the extended 
    /// properties. The TaskData class provides access to both types of storage
    /// using the same interface.</para>
    /// <para>To access data you can use the indexer of the TaskData class.</para>
    /// <code>
    /// TaskData taskData;
    /// string myValue = (string)taskData["MyValue"];
    /// </code>
    /// <para><b>Storing data as field values</b></para>
    /// <para>Since each task is stored as a list-item, it always has a 
    /// content-type attached. The content-type is either the base workflow 
    /// task, the WSS3 Workflow Tools task or a custom 
    /// workflow task content-type. Using a custom content-type allows you 
    /// to define the task data using site-columns and is the preferred method
    /// of storing task-data. The TaskData class provides access to the field
    /// data for the content-type using three types of indexer keys, 
    /// all resolving to the same data.</para>
    /// <list>
    ///     <item>The internal name of the field in the task content-type.</item>
    ///     <item>The GUID ID of the field in the task content-type in a <see cref="System.Guid"/> format.</item>
    ///     <item>The GUID ID of the field in the task content-type in a <see cref="System.String"/> format.</item>    
    /// </list>
    /// <para>When the data is stored inside a specific field called MyField 
    /// with an ID of {000000-0000-0000-000000000000}, the following code 
    /// would write "True" to the console.</para>
    /// <code>
    /// TaskData taskData;
    /// object o1 = taskData["MyField"];
    /// object o2 = taskData["{000000-0000-0000-000000000000}"];
    /// object o2 = taskData[new Guid("{000000-0000-0000-000000000000}")];
    /// Console.WriteLine(o1 == o2 == o3);
    /// </code>
    /// The fields of the task list-item are always accessible using these 
    /// three methods. Note that only the fields defined in the content-type
    /// can be accessed. 
    /// <para><b>Storing data in the extended properties</b></para>
    /// <para>To use the extended property collection you use a random string
    /// as a key. The value should not correspond with the internal name of a 
    /// field defined in the content-type attached to the list-item. Although
    /// the data would be updated, you would not be using the extended property
    /// collection.</para>
    /// <code>
    /// TaskData taskData;
    /// object o1 = taskData["MyCustomKeyName"];
    /// </code>
    /// <para>The TaskData class controls access to the extended property 
    /// collection using the <see cref="TaskData.AllowExtendedProperties" />
    /// property. When the value is set to <b>false</b> the use of custom keys
    /// is not allowed and data is only accessible through the content-type.
    /// An important note is that this is not a security mechanism. Instead it 
    /// serves as a notification mechanism when accessing data in an unintended
    /// manner</para>
    /// <para>Default Value for AllowExtendedProperties</para>
    /// <para>Although you can control the value of the <see cref="TaskData.AllowExtendedProperties" />
    /// property through code, the framework tries to make a best guess as to 
    /// what is likely to be necessary. When you create the task using the 
    /// content-type provided by WSS3 Workflow Tools the framework will allow
    /// the extended properties by default. In the case of a custom content-type
    /// or the built-in SharePoint workflow task type the framework
    /// will disallow the use of extended properties.</para>
    /// </remarks>
    [TypeDescriptionProvider(typeof(TaskDataDescriptionProvider))]
    public partial class TaskData
    {
        #region [===== Fields =====]
        private Dictionary<object, SPField> _contentTypePropertyKeys;
        #endregion

        #region [===== Properties =====]
        /// <summary>
        /// Gets or sets a value indicating if random keys are allowed to 
        /// access task-data.
        /// </summary>
        public bool AllowExtendedProperties { get; set; }
        /// <summary>
        /// Gets the <see cref="SPContentType" /> assigned to the task 
        /// list-item.
        /// </summary>
        public SPContentType ContentType { get; private set; }

        /// <summary>
        /// Gets or sets a task data value.
        /// </summary>
        /// <param name="key">The key used to store the data.</param>
        /// <returns>The data</returns>        
        public object this[object key]
        {
            get { return GetProperty(key); }
            set { SetProperty(key, value); }
        }

        internal bool ContentTypePropertiesChanged { get; private set; }
        internal bool ExtendedPropertiesChanged { get; private set; }
        internal Hashtable ExtendedProperties { get; private set; }
        internal Dictionary<Guid, object> ContentTypeFieldValues { get; private set; }
        internal IEnumerable<object> Keys
        {
            get
            {
                foreach (object key in _contentTypePropertyKeys.Keys)
                {
                    yield return key;
                }
                foreach (object key in ExtendedProperties.Keys)
                {
                    yield return key;
                }
            }
        }
        #endregion

        #region [===== Constructors =====]
        /// <summary>
        /// Initializes a new instance of the TaskData class using the 
        /// list item which is currently in context.
        /// </summary>
        /// <remarks>
        /// The list-item used to initialize with is read from the following
        /// container.
        /// <code>SPContext.Current.ListItem</code>
        /// </remarks>
        public TaskData()
            : this(SPContext.Current.ListItem,
            SPWorkflowTask.GetExtendedPropertiesAsHashtable(SPContext.Current.ListItem))
        {
            AllowExtendedProperties = true;
        }

        internal TaskData(SPListItem listItem, Hashtable extendedProperties)
            : this(listItem.ContentType)
        {
            foreach (object key in _contentTypePropertyKeys.Keys)
            {
                Guid? guidKey = key as Guid?;
                if (guidKey != null)
                {
                    ContentTypeFieldValues.Add(
                        guidKey.Value, listItem[guidKey.Value]);
                }
            }
            ExtendedProperties = extendedProperties;
        }

        internal TaskData(SPContentType contentType)
        {
            _contentTypePropertyKeys = new Dictionary<object, SPField>();
            ContentTypeFieldValues = new Dictionary<Guid, object>();
            ExtendedProperties = new Hashtable();
            LoadContentTypeFields(contentType);
        }
        #endregion

        #region [===== Internal methods =====]
        internal void ResetStatus()
        {
            ContentTypePropertiesChanged = false;
            ExtendedPropertiesChanged = false;
        }
        #endregion

        #region [===== Private methods =====]
        void LoadContentTypeFields(SPContentType contentType)
        {
            foreach (SPField field in contentType.Fields)
            {
                if (field.Hidden == false)
                {
                    _contentTypePropertyKeys.Add(field.Id, field);
                    _contentTypePropertyKeys.Add(
                        field.Id.ToString(), field);
                    _contentTypePropertyKeys.Add(
                        field.Id.ToString("B"), field);
                    _contentTypePropertyKeys.Add(
                        field.InternalName, field);
                }
            }
        }

        object GetProperty(object key)
        {
            object value = null;
            if (_contentTypePropertyKeys.ContainsKey(key))
            {
                SPField field = _contentTypePropertyKeys[key];
                value = ContentTypeFieldValues[field.Id];
            }
            else if (AllowExtendedProperties)
            {
                value = ExtendedProperties[key];
            }
            else
            {
                throw new NotSupportedException("Key not present in task content-type and extended properties not allowed");
            }
            return value;
        }

        void SetProperty(object key, object value)
        {
            if (_contentTypePropertyKeys.ContainsKey(key))
            {
                SPField field = _contentTypePropertyKeys[key];
                ContentTypeFieldValues[field.Id] = value;
                ContentTypePropertiesChanged = true;
            }
            else if (AllowExtendedProperties)
            {
                ExtendedProperties[key] = value;
                ExtendedPropertiesChanged = true;
            }
            else
            {
                throw new NotSupportedException("Key not present in task content-type and extended properties not allowed");
            }
        }
        #endregion
    }    
}
